/*	--- 代理 --- 

#	为【其他对象】提供一种【代理】以【控制】对【这个对象的访问】。

=======================================================


【代理模式】，即Proxy，它和Adapter模式很类似。

我们先回顾【Adapter模式】，它用于把【A接口】转换为【B接口】： */
public B_Adapter implements B {
	private A a;

	public B_Adapter(A a) {
		this.a = a;
	}

	public void b() {
		a.a();
	}
}

/*
而【Proxy模式】不是把【A接口】转换成【B接口】，它还是转换成【A接口】： */
public A_Proxy implements A {
	private A a;

	public A_Proxy(A a) {
		this.a = a;
	}

	public void a() {
		this.a.a();
	}
}


/*
合着Proxy就是为了给A接口再包一层，这不是脱了裤子放屁吗？

当然不是。

我们观察【Proxy】的【实现A接口的方法】： */
public void a() {
	this.a.a();
}

/*
这样写当然没啥卵用。

但是，如果我们在调用a.a()的前后，加一些额外的代码： */
public void a() {
	if (getCurrentUser().isRoot()) {
		this.a.a();
	} else {
		throw new SecurityException("Forbidden");
	}
}


/*
这样一来，我们就实现了【权限检查】，只有符合要求的用户，才会真正调用目标方法，否则，会直接抛出异常。


-----------------------------

有的童鞋会问，为啥不把【权限检查的功能】直接写到【目标实例A的内部】？

因为我们【编写代码的原则】有：
	· 职责清晰：【一个类】只负责【一件事】；
	· 易于测试：【一次】只测【一个功能】。
	

用【Proxy】实现这个【权限检查】，我们可以获得更清晰、更简洁的代码：
	· Interface A：只定义接口；
	· A_Business类：只实现A接口的业务逻辑；
	· A_PermissionProxy类：只实现A接口的权限检查代理。


如果我们希望编写其他类型的代理，可以继续增加类似ALogProxy，而不必对现有的A接口、ABusiness类进行修改。

实际上权限检查只是代理模式的一种应用。Proxy还广泛应用在：


========================================================

#	远程代理

【远程代理】即Remote Proxy，【本地的调用者】持有的【接口】实际上是【一个代理】，【这个代理】负责把【对接口的方法访问】转换成【远程调用】，然后【返回结果】。

Java内置的【RMI机制】就是一个【完整的远程代理模式】。


=======================================================

#	虚代理

【虚代理】即Virtual Proxy，它让【调用者】先持有一个【代理对象】，但【真正的对象】尚未创建。

如果没有必要，这个【真正的对象】是【不会被创建】的，直到【客户端】需要真的必须调用时，才创建真正的对象。

【JDBC的连接池】返回的【JDBC连接】（Connection对象）就可以是【一个虚代理】，即【获取连接时】根本没有【任何实际的数据库连接】，直到【第一次执行JDBC查询】或【更新操作】时，才【真正创建】实际的JDBC连接。


==================================================

#	保护代理

【保护代理】即Protection Proxy，它用【代理对象】控制对【原始对象的访问】，常用于【鉴权】。
=

===================================================

#	智能引用

【智能引用】即Smart Reference，它也是一种【代理对象】，如果有【很多客户端】对它进行访问，通过【内部的计数器】可以在【外部调用者】都【不使用后】自动释放它。


=======================================================


我们来看一下如何应用【代理模式】编写一个【JDBC连接池】（DataSource）。

我们首先来编写一个【虚代理】，即如果【调用者】获取到【Connection】后，并没有执行任何SQL操作，那么这个【Connection Proxy】实际上并不会真正打开【JDBC连接】。

【虚代理】 - 【调用者代码】如下： */
DataSource lazyDataSource = new LazyDataSource(jdbcUrl, jdbcUsername, jdbcPassword);
System.out.println("get lazy connection...");

try (Connection conn1 = lazyDataSource.getConnection()) {
	// 并没有实际打开真正的Connection
}
System.out.println("get lazy connection...");

try (Connection conn2 = lazyDataSource.getConnection()) {
	try (PreparedStatement ps = conn2.prepareStatement("Select * From students")) { // 打开了真正的Connection
		try (ResultSet rs = ps.executeQuery()) {
			while (rs.next()) {
				System.out.println(rs.getString("name"));
			}
		}
	}
}

/*
现在我们来思考【如何实现】这个【LazyConnectionProxy】。

为了简化代码，我们首先针对【Connection接口】做一个【抽象的代理类】：

实现LazyConnectionProxy第1步：
    对【Connection接口】进行【抽象】,做成一个【Proxy】 */
public abstract class AbstractConnectionProxy implements Connection {
	// 抽象方法: 获取实际的Connection
	protected abstract Connection getRealConnection();

	// 实现Connection接口的每一个方法
	public Statement createStatement() throws SQLException {
		return getRealConnection().createStatement();
	}

	public PreparedStatement prepareStatement(String sql) throws SQLException {
		return getRealConneciotn().preapreStatement(sql);
	}

	// ...其他代理方法...
}


/*
这个【AbstractConnectionProxy 代理类】的【作用】是把【Connection接口定义的方法】全部实现一遍，因为【Connection接口】定义的方法太多了，后面我们要编写的【LazyConnectionProxy】只需要【继承AbstractConnectionProxy】，就不必再把【Connection接口方法】挨个实现一遍。

实现LazyConnectionProxy第2步：
	编写【LazyConnectionProxy实现】如下： */
public class LazyConnectionProxy extends AbstractConnectionProxy {
	private Supplier<Connection> supplier;
	private Connection target = null; // Supplier.get()获取

	public LazyConnectionProxy(Supplier<Connection> supplier) {
		this.supplier = supplier; // return Connection
	}

	// 覆写close方法：只有target != null时才需要关闭
	public void close() throws SQLException {
		if (target != null) {
			System.out.println("Close connection: " + target);
			super.close();
		}
	}

	@Override
	protected Connection getRealConnection() {
		if (target == null) {
			target = supplier.get();
		}

		return target;
	}
}


/*
如果【调用者】没有【执行任何SQL语句】，那么【target字段】始终为【null】。

只有【第一次执行SQL语句】时（即调用任何类似prepareStatement()方法时，触发getRealConnection()调用），才会真正打开【实际的JDBC Connection】。

最后，我们还需要编写一个【LazyDataSource】来支持这个【LazyConnecitonProxy】： */
public class LazyDataSource implements DataSource {

    private String url;
    private String username;
    private String password;

    // init
    public LazyDataSource(String url, String username, String password) {
        this.url = url;
        this.username = username;
        this.password = password;
    }

    // 获取链接
    public Connection getConnection(String username, String password) throws SQLException {
        return new LazyConnectionProxy(() -> { 
        // LazyConnectionProxy
            try {
                Connection conn = DriverManager.getConnection(url, username, password);
                System.out.println("Open connection: " + conn);

                return conn;

            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        });
    }
    //...
}

/*
我们执行【调用者】代码 */
DataSource lazyDataSource = new LazyDataSource(jdbcUrl, jdbcUsername, jdbcPassword);
System.out.println("get lazy connection...");

try (Connection conn1 = lazyDataSource.getConnection()) {
	// 并没有实际打开真正的Connection
}
System.out.println("get lazy connection...");

try (Connection conn2 = lazyDataSource.getConnection()) {
	try (PreparedStatement ps = conn2.prepareStatement("Select * From students")) { // 打开了真正的Connection
		try (ResultSet rs = ps.executeQuery()) {
			while (rs.next()) {
				System.out.println(rs.getString("name"));
			}
		}
	}
}

/*
输出如下：

	get lazy connection...
	get lazy connection...
Y-> Open connection: com.mysql.jdbc.JDBC4Connection@7a36aefa
	小明  <-- while(rs.next())
	小红
	小军
	小白
	...
	Close connection: com.mysql.jdbc.JDBC4Connection@7a36aefa

------------------

可见【第一个getConnection()】调用【获取到的LazyConnectionProxy】并没有【实际打开】真正的 JDBC Connection。

使用【连接池】的时候，我们更希望能【重复使用连接】。

如果【调用方】编写这样的代码： */
DataSource pooledDataSource = new PooledDataSource(jdbcUrl, jdbcUsername, jdbcPassword);

try (Connection conn = pooledDataSource.getConnection()) {
	//...
}

try (Connection conn = pooledDataSource.getConnection()) {
	// 获取到的是同一个Connection
}

try (Connection conn = pooledDataSource.getConnection()) {
	// 获取到的是同一个Connection
}


/*
【调用方】并【不关心】是否【复用了Connection】，但从【PooledDataSource】获取的【Connection】确实自带这个【优化功能】。

------------------------------------------------

Q: 如何实现【可复用Connection的连接池】？
A: 答案仍然是【使用代理模式】。	*/
public class PooledConnectionProxy extends AbstractConnectionProxy { // (1)

	// 实际的Connection
	Connection target;

	// 空闲队列
	Queue<PooledConnectionProxy> idleQueue;

	public PooledConnectionProxy(Queue<PooledConnectionProxy> idleQueue, Connection target) {
		this.idleQueue = idleQueue;
		this.target = target; // target  n.  对象
	}

	public void close() throws SQLException {
		System.out.println("Fake close and realeased to idle queue for future reuse: " + target);
		// 并没有调用【实际的Connection.close()方法】

		// 而是把【Connection自己】放入了【空闲队列】
		idleQueue.offer(this); 
			// offer v. 自愿给予,右供应左
	}

	protected Connection getRealConnection() {
		return target;
	}
}


/*
【复用连接】的关键在于【覆写close()方法】，它并没有真正关闭【底层JDBC连接】，而是把自己放回一个【空闲队列】，以便下次使用。

【空闲队列】由【PooledDataSource】负责【维护】： */
public class PooledDataSource implements DataSource {
// PooledDataSource -> PooledConnectionProxy
// 封装【PooledConnectionProxy】

	private String url;
	private String username;
	private String password;

	// 【维护】一个【空闲队列 idleQueue】
	private Queue<PooledConnectionProxy> idleQueue = new ArrayBlockingQueue<>(100); // unique Queue

	public PooledDataSource(String url, String uername, String password) {
		this.url = url;
		this.username = username;
		this.password = password;
	}

	public Connection getConnection(String username, String password) throws SQLException {
		// 首先试图获取一个【空闲连接】
		PooledConnectionProxy conn = idleQueue.poll();
			// poll v. 投出;  
			// Pooled adj. 集合,池化
			// conn = PooledConnectionProxy

		if (conn == null) {
			// 没有【空闲连接】时, 打开一个【新连接】
			conn = openNewConnection(); // private Method
				// return new PoolConnectionProxy 

		} else {
			System.out.println("Return pooled connection: " + conn.target);
			// conn.target == PooledConnectionProxy.Connection
		}

		return conn;
			// return PooledConnectionProxy
	}

	private PooledConnectionProxy openNewConnection() throws SQLException {

		Connection conn = DriverManager.getConnection(url, username, password);
		System.out.println("Open new connection: " + conn);

		return new PooledConnectionProxy(idleQueue, conn);

	}

	// ....
}

/*
我们执行【调用方代码】，输出如下： 

Open new connection: com.mysql.jdbc.JDBC4Connection@61ca2dfa
Fake close and released to idle queue for future reuse: com.mysql.jdbc.JDBC4Connection@61ca2dfa
Return pooled connection: com.mysql.jdbc.JDBC4Connection@61ca2dfa
Fake close and released to idle queue for future reuse: com.mysql.jdbc.JDBC4Connection@61ca2dfa
Return pooled connection: com.mysql.jdbc.JDBC4Connection@61ca2dfa
Fake close and released to idle queue for future reuse: com.mysql.jdbc.JDBC4Connection@61ca2dfa


除了【第一次】打开了【一个真正的JDBC Connection】，【后续获取的Connection】实际上是【同一个JDBC Connection】。

但是，对于【调用方】来说，完全不需要知道【底层】做了【哪些优化】。

--------------------------

我们【实际使用的DataSource】，例如【HikariCP】，都是基于【代理模式】实现的，【原理】同上，但【增加了】更多的如【动态伸缩的功能】（一个【连接】空闲一段时间后【自动关闭】）。


有的童鞋会发现【Proxy模式】和【Decorator模式】有些类似。

确实，这两者看起来很像，但区别在于：【Decorator模式】让【调用者】自己创建【核心类】，然后组合【各种功能】，

而【Proxy模式】决不能让【调用者】自己【创建再组合】，否则就失去了【代理的功能】。

【Proxy模式】让【调用者】【认为】获取到的是【核心类接口】，但实际上是【代理类】。



=====================================================

#	练习

使用【代理模式】编写一个【JDBC连接池】 */




/*=======================================================

#	----- 代理 Proxy の 小结 -----


1. 【代理模式】通过【封装】一个【已有接口】，并向【调用方】返回【相同的接口类型】，能让【调用方】在【不改变任何代码的前提下】增强【某些功能】（例如，鉴权、延迟加载、连接池复用等）。

2. 使用【Proxy模式】要求【调用方】持有【接口】，作为【Proxy的类】也必须实现【相同的接口类型】。




*/ 